--
-- 分布式ID生成脚本
-- User: allen.liu
-- Date: 2019/5/5
-- Time: 14:35
-- To change this template use File | Settings | File Templates.
--

-- Redis结构
--      current_value: 当前ID值
--      max_value_available: 最大可用ID值
-- 统计信息结构
--      当前时间秒数:当前值

-- 获取统计数据Hash值
-- @param fieldName 原始Key值
-- @return 统计Hash的Key值
local function internalGetStatisticKey(fieldName)
    return fieldName .. ':statistic';
end

-- 获取n分钟之内的统计数据
-- 如果未获取到统计值, 则默认每分钟200次消费
-- @param fieldName 原始key值
-- @param currentMilliSeconds 当前时间秒数
-- @param lastMinutes 最近多少分钟以内的统计数据
-- @return n分钟之内的统计数据
local function internalGetStatisticValue(fieldName, currentMilliSeconds, lastMinutes)
    local lastMinutesValue = redis.pcall('HGET', internalGetStatisticKey(fieldName), currentMilliSeconds - lastMinutes * 60 * 1000);
    if (type(lastMinutesValue) == 'boolean' or lastMinutesValue == nil) then
        return 200 * lastMinutes;
    end
    return lastMinutesValue;
end


-- 刷新最大可用值
-- 如果当前值距最大可用值超出了15分钟消费的需求, 则增加至1小时的最大可用量
-- 如果当前值不存在, 则默认出现了异常(可能是Redis重启之后数据丢失), 则更新当前值为最大可用值, 然后再利用算法统一刷新最大可用值
-- @param fieldName Key
-- @param currentValue 当前ID值
-- @param currentMilliSeconds 当前时间秒数
-- @return 刷新之后的最大可用值
local function internalRefreshMaxValueAvailable(fieldName, step, currentMilliSeconds)
    -- 获取最大可用ID
    local maxValueAvailable = redis.pcall('HGET', fieldName, 'max_value_available');
    if (type(maxValueAvailable) == 'boolean' or maxValueAvailable == nil) then
        return nil;
    end

    -- 获取当前ID, 如果不存在, 就初始化为最大可用值
    local currentValue = redis.pcall('HGET', fieldName, 'current_value');
    if (type(currentValue) == 'boolean' or currentValue == nil) then
        currentValue = maxValueAvailable;
        redis.pcall('HSET', fieldName, 'current_value', currentValue);
    end

    -- 获取最近5分钟统计值
    local lastFiveMinutesSts = internalGetStatisticValue(fieldName, currentMilliSeconds, 5);

    -- 获取最近30分钟统计值
    local lastThirtyMinutesSts = internalGetStatisticValue(fieldName, currentMilliSeconds, 30);

    -- 初始化步长
    if (step == nil) then
        step = 1;
    end

    -- 如果当前值距最大可用值超出了15分钟消费的需求, 则增加至1小时的最大可用量
    if (maxValueAvailable - currentValue < lastFiveMinutesSts * 3 * step) then
        maxValueAvailable = redis.pcall('HINCRBY', fieldName, 'max_value_available', lastThirtyMinutesSts * step * 2);
    end

    return maxValueAvailable;
end

-- 获取分布式ID
-- @param fieldName Key
-- @param step 步长
-- @param currentMilliSeconds 当前时间秒数
-- @param maxValueAvailableFromDB 数据库中的最大可用值
-- @return success: 当前值,最大可用值,刷新之后的最大可用值 failed: nil
local function getDistributionId(fieldName, step, currentMilliSeconds, maxValueAvailableFromDB)
    -- 获取最大可用ID
    local maxValueAvailable = redis.pcall('HGET', fieldName, 'max_value_available');
    if (type(maxValueAvailable) == 'boolean' or maxValueAvailable == nil) then
        if (maxValueAvailableFromDB == nil) then
            -- 直接返回nil
            return nil;
        else
            -- 初始化最大可用值为DB值
            maxValueAvailable = maxValueAvailableFromDB;
            redis.pcall('HSET', fieldName, 'max_value_available', maxValueAvailable);
        end
    end

    -- 刷新最大可用值
    local maxValueAvailableRefresh = internalRefreshMaxValueAvailable(fieldName, step, currentMilliSeconds);


    -- 获取当前ID
    local currentValue = redis.pcall('HINCRBY', fieldName, 'current_value', step);
    if (type(currentValue) == 'boolean' or currentValue == nil) then
        return nil;
    end

    -- 校验最大可用ID
    if (type(maxValueAvailableRefresh) == 'boolean' or maxValueAvailableRefresh == nil) then
        return nil;
    elseif tonumber(maxValueAvailableRefresh) < tonumber(currentValue) then
        return nil;
    end

    -- 统计信息
    redis.pcall('HSET', internalGetStatisticKey(fieldName), currentMilliSeconds, currentValue);

    return currentValue .. ',' .. maxValueAvailable .. ',' .. maxValueAvailableRefresh;
end


-- 主程序
local fieldName = KEYS[1]
local method = ARGV[1]
print('fieldName: ' .. fieldName .. ', method: ' .. method);
if method == 'getDistributionId' then
    return getDistributionId(fieldName, tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4]));
else
    --ignore
    print('Ignore: nil');
    return nil;
end